// Package repository
/**
* @Project : geektime-basic-go-study
* @File    : user.go
* @IDE     : GoLand
* @Author  : Tvux
* @Date    : 2024/9/5 13:05
**/

package repository

import (
	"context"
	"database/sql"
	"geektime-basic-go-study/webook/backend/internal/domain"
	"geektime-basic-go-study/webook/backend/internal/repository/cache"
	"geektime-basic-go-study/webook/backend/internal/repository/dao"
	"github.com/HJH0924/GenericGo/sqlx"
	"log"
	"time"
)

var (
	ErrUserDuplicate = dao.ErrUserDuplicate
	ErrUserNotFound  = dao.ErrUserNotFound
)

type UserRepository interface {
	Create(ctx context.Context, u domain.User) error
	Update(ctx context.Context, u domain.User) error
	FindUserByEmail(ctx context.Context, email string) (domain.User, error)
	FindUserById(ctx context.Context, id int64) (domain.User, error)
	FindUserByPhone(ctx context.Context, phone string) (domain.User, error)
	FindUserByWechat(ctx context.Context, openId string) (domain.User, error)
}

var (
	_ UserRepository = (*CachedUserRepository)(nil)
)

type CachedUserRepository struct {
	ud dao.UserDAO
	uc cache.UserCache
}

func NewCachedUserRepository(ud dao.UserDAO, uc cache.UserCache) UserRepository {
	return &CachedUserRepository{
		ud: ud,
		uc: uc,
	}
}

func (Self *CachedUserRepository) Create(ctx context.Context, u domain.User) error {
	return Self.ud.Insert(ctx, Self.domainToDao(u))
}

func (Self *CachedUserRepository) Update(ctx context.Context, u domain.User) error {
	err := Self.ud.UpdateNonZeroFields(ctx, Self.domainToDao(u))
	if err != nil {
		return err
	}
	// 这里有两种策略：
	// 1、删除缓存中对应数据 缓存失效，下次查询的时候从数据库获取最新数据并回写缓存（更优）
	// 2、在这里刷新缓存，下次查询的时候就从缓存中获取
	return Self.uc.Delete(ctx, u.Id)
}

func (Self *CachedUserRepository) FindUserByEmail(ctx context.Context, email string) (domain.User, error) {
	u, err := Self.ud.FindUserByEmail(ctx, email)
	if err != nil {
		return domain.User{}, err
	}
	return Self.daoToDomain(u), nil
}

func (Self *CachedUserRepository) FindUserById(ctx context.Context, id int64) (domain.User, error) {
	// 先从缓存中，找不到再去数据库找，找到了回写缓存
	user, err := Self.uc.Get(ctx, id)
	if err == nil {
		// 错误为空，必然有数据，直接返回
		return user, nil
	}
	// ---
	// 这里先考虑简单实现，缓存找不到，或者缓存崩了，就都去数据库查询
	userFromDb, err := Self.ud.FindUserById(ctx, id)
	if err != nil {
		return domain.User{}, err
	}

	user = Self.daoToDomain(userFromDb)

	go func() {
		// 开一个 Goroutine 会不会有一致性问题？答：不开也会有一致性问题，开与不开问题不大
		//
		// 回写 Redis，便于下次查询同 ID 用户
		err = Self.uc.Set(ctx, user)
		if err != nil {
			// 这里可以不用返回错误，日志记录错误，做好监控即可
			log.Printf("FindUserById set cache failed, id = %d\n", id)
		}
	}()

	return user, nil
	// ---

	//if err == cache.ErrKeyNotExist {
	//	// 缓存里没有，去数据库里查询
	//}
	// 缓存崩了，那还需要从数据库加载数据吗？
	// 需要，这个时候需要保护住数据库，不然请求太多，都请求数据库，数据库也可能会崩
	// 使用数据库限流
}

func (Self *CachedUserRepository) FindUserByPhone(ctx context.Context, phone string) (domain.User, error) {
	u, err := Self.ud.FindUserByPhone(ctx, phone)
	if err != nil {
		return domain.User{}, err
	}
	return Self.daoToDomain(u), nil
}

func (Self *CachedUserRepository) FindUserByWechat(ctx context.Context, openId string) (domain.User, error) {
	u, err := Self.ud.FindUserByWechat(ctx, openId)
	if err != nil {
		return domain.User{}, err
	}
	return Self.daoToDomain(u), nil
}

func (Self *CachedUserRepository) daoToDomain(u dao.User) domain.User {
	var birthday time.Time
	if u.Birthday.Valid {
		birthday = time.UnixMilli(u.Birthday.Int64)
	}
	return domain.User{
		Id:       u.Id,
		Email:    u.Email.String,
		Password: u.Password,
		Phone:    u.Phone.String,
		Nickname: u.Nickname.String,
		Birthday: birthday,
		AboutMe:  u.AboutMe.String,
		WechatInfo: domain.WechatUserInfo{
			OpenId:  u.WechatOpenId.String,
			UnionId: u.WechatUnionId.String,
		},
	}
}

func (Self *CachedUserRepository) domainToDao(u domain.User) dao.User {
	return dao.User{
		Id:            u.Id,
		Email:         sqlx.NewNullString(u.Email),
		Password:      u.Password,
		Phone:         sqlx.NewNullString(u.Phone),
		Nickname:      sqlx.NewNullString(u.Nickname),
		Birthday:      sql.NullInt64{Int64: u.Birthday.UnixMilli(), Valid: !u.Birthday.IsZero()},
		AboutMe:       sqlx.NewNullString(u.AboutMe),
		WechatOpenId:  sqlx.NewNullString(u.WechatInfo.OpenId),
		WechatUnionId: sqlx.NewNullString(u.WechatInfo.UnionId),
	}
}
